package org.chemodansama.fifteen;

import static org.chemodansama.engine.render.imgui.NpGuiReturnConsts.clicked;

import java.io.IOException;
import java.io.InputStream;
import java.nio.FloatBuffer;

import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import javax.microedition.khronos.opengles.GL11;
import javax.microedition.khronos.opengles.GL11ExtensionPack;

import org.chemodansama.engine.LogHelper;
import org.chemodansama.engine.LogTag;
import org.chemodansama.engine.NpGame;
import org.chemodansama.engine.NpGameState;
import org.chemodansama.engine.NpSoundMan;
import org.chemodansama.engine.math.NpMath;
import org.chemodansama.engine.math.NpMatrix4;
import org.chemodansama.engine.math.NpRect;
import org.chemodansama.engine.math.NpVec3;
import org.chemodansama.engine.math.NpVec4;
import org.chemodansama.engine.render.NpCamera;
import org.chemodansama.engine.render.NpMesh;
import org.chemodansama.engine.render.NpTexture;
import org.chemodansama.engine.render.imgui.NpAlignConsts;
import org.chemodansama.engine.render.imgui.NpFontParams;
import org.chemodansama.engine.render.imgui.NpGuiState;
import org.chemodansama.engine.render.imgui.NpSkin;
import org.chemodansama.engine.render.imgui.NpWidgetState;
import org.chemodansama.engine.utils.NpByteBuffer;
import org.chemodansama.engine.utils.NpUtils;
import org.chemodansama.fifteen.Panels.NpClickListener;

import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.res.AssetManager;
import android.text.InputFilter;
import android.text.Spanned;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.EditText;

final class ChipFieldView {
    
    private enum ChipRenderEnum {ALL, SLIDING, NOT_SLIDING};
    
    private final static String CUBE_MAP_EXT_NAME = "GL_OES_texture_cube_map";
    private final static String CHIP_MESH_XML = "meshes/chip.xml";
    
    private final static String BOX_DIFFUSE_MAP = "textures/box15.tf";
    private final static String BOX_NORMAL_MAP = "textures/box15n.tf";
    private final static String BOX_MESH_XML = "meshes/15box.xml";
    private final static int BOX_DEFAULT_DIM = 4;
    
    private static final NpVec4 SLIDING_COLOR = new NpVec4(0.75f, 0.75f, 0.75f, 
                                                           1);
    
    private static boolean checkCubemapSupport(GL10 gl) {
        return checkExtensionSupport(gl, CUBE_MAP_EXT_NAME);
    }

    private static boolean checkExtensionSupport(GL10 gl, String extension) {
        String s = gl.glGetString(GL10.GL_EXTENSIONS);
        return s.indexOf(extension) >= 0;
    }
    
    private final AssetManager mAssets;
    
    private final int mDim;
    private final int mN;
    
    private final boolean mContextSupportsCubemaps;
    private final float[] mLightPos;
    private NormalisationCubeMap mCubeMap;
    
    private final ChipField mField;

    private final ChipMatrixStruct[] mChipMatrices;
    private final NpMesh mChipMesh;
    private final NpTexture[] mChipDiffuseMaps;
    private final NpTexture[] mChipNormalMaps;
    
    private final NpMesh mBoxMesh;
    private NpTexture mBoxDiffuse = null;
    private NpTexture mBoxNormalMap = null;
    private final NpMatrix4 mBoxWorldMat = new NpMatrix4();
    private final NpMatrix4 mBoxWorldViewMat = new NpMatrix4();
    private final NpMatrix4 mBoxInverseWorldMat = new NpMatrix4();
    
    private float[] mBoxTangentSpace;
    private FloatBuffer mBoxTangentSpaceBuf;
    private final float[][] mChipTangentSpaceLight;
    private final FloatBuffer[] mChipTangentSpaceLightBuf;
    
    private final GameBackGroundView mBG;
    private boolean mValid = false;
    
    ChipFieldView(GL10 gl, Activity activity, AssetManager assets, 
                  ChipField field) {
        super();
        
        mDim = field.Dim;
        mN = field.N;
        
        mLightPos = new float[] {mDim * 0.5f, -mDim * 0.5f, 2, 1};
        
        mChipMatrices = new ChipMatrixStruct[mN];
        mChipDiffuseMaps = new NpTexture[mN];
        mChipNormalMaps = new NpTexture[mN];
        
        mChipTangentSpaceLight = new float[mN][];
        mChipTangentSpaceLightBuf = new FloatBuffer[mN];
        
        mAssets = assets;
        mField = field;

        mContextSupportsCubemaps = checkCubemapSupport(gl);
        
        InputStream in;
        
        // Load our chip mesh, 
        // and prepare for normal mappin' in case context supports cube maps.
        in = NpUtils.openStreamNoCatch(mAssets, CHIP_MESH_XML); 
        mChipMesh = new NpMesh(in, mContextSupportsCubemaps);
        
        in = NpUtils.openStreamNoCatch(mAssets, BOX_MESH_XML);
        mBoxMesh = new NpMesh(in, mContextSupportsCubemaps);

        allocateTangentSpaceLight();
                
        for (int i = 0; i < mChipMatrices.length; i++) {
            mChipMatrices[i] = new ChipMatrixStruct(mDim);
        }
        
        mBG = new GameBackGroundView(activity, assets, gl, mDim * 0.5f, -mDim * 0.5f);
    }
    
    private void allocateTangentSpaceLight() {
        
        if (!mContextSupportsCubemaps) {
            return;
        }
        
        int coordsLen = mChipMesh.getCoordsLen();
        
        if (coordsLen <= 0) {
            return;
        }

        // little optimization tweak goes here :D
        int limit = mChipTangentSpaceLight.length;
        
        for (int i = 0; i < limit; i++) {
            mChipTangentSpaceLight[i] = new float[coordsLen];
            
            mChipTangentSpaceLightBuf[i] = 
                NpByteBuffer.allocateDirectNativeFloat(coordsLen); 
        }
        
        int boxCoordsLen = mBoxMesh.getCoordsLen();
        
        mBoxTangentSpace = new float[boxCoordsLen];
        
        mBoxTangentSpaceBuf = 
            NpByteBuffer.allocateDirectNativeFloat(boxCoordsLen);
        
    }
    
    private void doAmbientPass(GL10 gl) {
        
        gl.glColor4f(0.25f, 0.25f, 0.5f, 1);
        
        gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
        gl.glDisable(GL10.GL_TEXTURE_2D);
        
        renderChipCells(gl, null, null);
        
        mBoxDiffuse.bindGL10(gl);
        
        gl.glPushMatrix();
        gl.glLoadMatrixf(mBoxWorldViewMat.getArray(), 0);

        mBoxMesh.setupArraysPointers(gl, false, false);
        mBoxMesh.draw(gl, false);
        
        gl.glPopMatrix();
        
        gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
        gl.glEnable(GL10.GL_TEXTURE_2D);
        
        gl.glColor4f(1, 1, 1, 1);
    }
    
    private void doLightPass(GL10 gl) {
        //Bind normalization cube map
        gl.glActiveTexture(GL10.GL_TEXTURE1);
        mCubeMap.bind(gl);
        gl.glEnable(GL11ExtensionPack.GL_TEXTURE_CUBE_MAP);

        GL11 gl11 = (GL11) gl;
        //Set up texture environment to do (tex0 dot tex1) * color
        gl11.glTexEnvx(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, 
                       GL11.GL_COMBINE);

        gl11.glTexEnvx(GL11.GL_TEXTURE_ENV, GL11.GL_SRC0_RGB, 
                       GL11.GL_PREVIOUS);

        gl11.glTexEnvx(GL11.GL_TEXTURE_ENV, GL11.GL_COMBINE_RGB, 
                       GL11.GL_DOT3_RGB);

        gl11.glTexEnvx(GL11.GL_TEXTURE_ENV, GL11.GL_SRC1_RGB, 
                       GL11.GL_TEXTURE);

        gl.glClientActiveTexture(GL10.GL_TEXTURE1);
        gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);

        //Bind normal map 
        gl.glActiveTexture(GL10.GL_TEXTURE0);
        
        gl.glEnable(GL10.GL_TEXTURE_2D);

        gl11.glTexEnvx(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, 
                       GL11.GL_COMBINE);

        gl11.glTexEnvx(GL11.GL_TEXTURE_ENV, GL11.GL_SRC0_RGB, 
                       GL11.GL_TEXTURE);

        gl11.glTexEnvx(GL11.GL_TEXTURE_ENV, GL11.GL_COMBINE_RGB, 
                       GL11.GL_REPLACE);

        gl.glClientActiveTexture(GL10.GL_TEXTURE0);

        gl.glColor4f(0.25f, 0.25f, 0.5f, 1);
        
        renderChipCells(gl, mChipNormalMaps, mChipTangentSpaceLightBuf);
        
        mBoxNormalMap.bindGL10(gl);
        
        gl.glPushMatrix();
        gl.glLoadMatrixf(mBoxWorldViewMat.getArray(), 0);

        mBoxMesh.setupArraysPointers(gl, true, false);
        
        gl.glClientActiveTexture(GL10.GL_TEXTURE1);
        gl.glTexCoordPointer(3, GL10.GL_FLOAT, 0, mBoxTangentSpaceBuf);
        gl.glClientActiveTexture(GL10.GL_TEXTURE0);
        
        mBoxMesh.draw(gl, false);
        
        gl.glPopMatrix();

        gl.glClientActiveTexture(GL10.GL_TEXTURE1);
        gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
        gl.glClientActiveTexture(GL10.GL_TEXTURE0);

        gl.glActiveTexture(GL10.GL_TEXTURE1);
        gl11.glDisable(GL11ExtensionPack.GL_TEXTURE_CUBE_MAP);
        gl.glActiveTexture(GL10.GL_TEXTURE0);

        gl.glTexEnvx(GL10.GL_TEXTURE_ENV, GL10.GL_TEXTURE_ENV_MODE, 
                     GL10.GL_MODULATE);
        
        gl.glColor4f(1, 1, 1, 1);
    }
    
    private void doNormalPass(GL10 gl) {
        renderChipCells(gl, mChipDiffuseMaps, null, ChipRenderEnum.NOT_SLIDING);
        
        gl.glColor4f(SLIDING_COLOR.getX(), SLIDING_COLOR.getY(), 
                     SLIDING_COLOR.getZ(), SLIDING_COLOR.getW());
        
        renderChipCells(gl, mChipDiffuseMaps, null, ChipRenderEnum.SLIDING);
        
        gl.glColor4f(1, 1, 1, 1);
        
        mBoxDiffuse.bindGL10(gl);
        
        gl.glPushMatrix();
        gl.glLoadMatrixf(mBoxWorldViewMat.getArray(), 0);

        mBoxMesh.setupArraysPointers(gl, true, false);
        mBoxMesh.draw(gl, false);
        
        gl.glPopMatrix();
    }
    
    public float[] getLightPos() {
        return mLightPos;
    }
    
    void loadAssets(GL10 gl) throws IOException {
        
        for (int i = 0; i < mN - 1; i++) {
            String prefix = "textures/" + Integer.toString(i + 1);
            String diffuse = prefix + ".tf"; 
            
            mChipDiffuseMaps[i] = new NpTexture(gl, diffuse, mAssets, true);
            
            if (mContextSupportsCubemaps) {
                mChipNormalMaps[i] = new NpTexture(gl, prefix + "n.tf", mAssets, 
                                                   true);
            }
        }
        
        mBoxDiffuse = new NpTexture(gl, BOX_DIFFUSE_MAP, mAssets, true);
        mBoxNormalMap = new NpTexture(gl, BOX_NORMAL_MAP, mAssets, true);
        mCubeMap = (mContextSupportsCubemaps) ? new NormalisationCubeMap(gl) 
                                              : null;
        
        mBG.loadAssets(gl);
        
        mValid = true;
    }
    
    void releaseAssets(GL10 gl) {
        mValid = false;
        
        for (int i = 0; i < mN - 1; i++) {
            mChipDiffuseMaps[i].releaseAssets(gl);
            
            if (mContextSupportsCubemaps) {
                mChipNormalMaps[i].releaseAssets(gl);
            }
        }
        
        mBoxDiffuse.releaseAssets(gl);
        mBoxNormalMap.releaseAssets(gl);

        if (mCubeMap != null) {
            mCubeMap.releaseAssets(gl);
        }
        
        mBG.releaseAssets(gl);
    }
    
    public int rayToChip(float[] a, float[] b) {
        
        final float radius = NpVec3.len(Chip.CENTER);
        
        final float[] chipPos = new float[4];

        final float[] u = new float[3];
        final float[] out1 = new float[3];
        final float[] out2 = new float[3];
        
        float[] chipQuad = new float[2 * 4];
        
        chipQuad[0] =  Chip.CENTER[0];
        chipQuad[1] = -Chip.CENTER[1];
        
        chipQuad[2] =  Chip.CENTER[0];
        chipQuad[3] =  Chip.CENTER[1];
        
        chipQuad[4] = -Chip.CENTER[0];
        chipQuad[5] =  Chip.CENTER[1];
        
        chipQuad[6] = -Chip.CENTER[0];
        chipQuad[7] = -Chip.CENTER[1];
        
        float[] chipQuadi = new float[3 * 4];
        for (int j = 0; j < 4; j++) {
            chipQuadi[j * 3 + 2] = 2 * Chip.CENTER[2];
        }
        
        float outDist = Float.MAX_VALUE;
        int outChip = -1;
        
        NpVec3.sub(b, a, u);
        NpVec3.normalize(u);
        
        for (int i = 0; i < mN; i++) {
            
            Chip c = mField.getChip(i);
            
            if (c.mNum == mN - 1) {
                continue;
            }
            
            mChipMatrices[i].mulVecWorld(Chip.CENTER, chipPos);
            NpVec4.clip(chipPos);

            // check against chip's bounding sphere
            if (!NpMath.calcLineSphereIntersection(a, u, chipPos, radius, 
                                                   out1, out2)) {
                continue;
            }
                
            // check against top plane of the chip
            for (int j = 0; j < 4; j++) {
                chipQuadi[j * 3 + 0] = chipQuad[j * 2 + 0] + chipPos[0];
                chipQuadi[j * 3 + 1] = chipQuad[j * 2 + 1] + chipPos[1];
            }
            
            if (!NpMath.calcLineConvexIntersection(a, u, chipQuadi, out1)) {
                continue;
            }
            
            float newDist = NpVec3.dist(out1, a); 
            
            if (outDist < newDist) {
                continue;
            }
            
            outDist = newDist;
            outChip = c.mNum;
        }
        
        if (Log.isLoggable(LogTag.TAG, Log.INFO)) {
            Log.i(LogTag.TAG, "intersects: " + Integer.toString(outChip));
        }
        
        return outChip;
    }
    
    void render(GL10 gl) {
        
        if (!mValid) {
            return;
        }
        
        gl.glEnable(GL10.GL_DEPTH_TEST);
        gl.glDepthFunc(GL10.GL_LEQUAL);

        mBG.render(gl);
        
        if (mContextSupportsCubemaps) {
            // pass with dot3-normal mapping
            doLightPass(gl);
            
            // good-old multi-pass with blending enabled.
            gl.glEnable(GL10.GL_BLEND);
            gl.glBlendFunc(GL10.GL_ONE, GL10.GL_ONE);
            
            doAmbientPass(gl);

            gl.glBlendFunc(GL10.GL_DST_COLOR, GL10.GL_ZERO);
        }
        
        // simple render with diffuse texture
        doNormalPass(gl);

        if (mContextSupportsCubemaps) {
            gl.glDisable(GL10.GL_BLEND);
        }
        
        gl.glDisable(GL10.GL_DEPTH_TEST);
    }
    
    private void renderChipCells(GL10 gl, NpTexture[] textures, 
            FloatBuffer[] tangetSpaceLightBuf) {
        renderChipCells(gl, textures, tangetSpaceLightBuf, ChipRenderEnum.ALL);
    }
    
    private void renderChipCells(GL10 gl, NpTexture[] textures, 
            FloatBuffer[] tangetSpaceLightBuf, ChipRenderEnum chipsToRender) {

        boolean needTexturing = textures != null;
        
        mChipMesh.setupArraysPointers(gl, needTexturing, false);
        
        for (int i = 0; i < mN; i++) {

            Chip chip = mField.getChip(i);

            // skip empty cell 
            if (chip.mNum == mN - 1) {
                continue;
            }

            boolean isSliding = mField.isSliding(chip.mNum); 
            
            if ((chipsToRender == ChipRenderEnum.SLIDING) && !isSliding) {
                continue;
            }
            
            if ((chipsToRender == ChipRenderEnum.NOT_SLIDING) && isSliding) {
                continue;
            }

            if (needTexturing) {
                textures[chip.mNum].bindGL10(gl);
            }
            
            gl.glPushMatrix();

            if (mChipMatrices[i].glLoadWorldMat(gl)) {
                
                if (tangetSpaceLightBuf != null) {
                    gl.glClientActiveTexture(GL10.GL_TEXTURE1);
                    gl.glTexCoordPointer(3, GL10.GL_FLOAT, 0, 
                                         tangetSpaceLightBuf[i]);
                    gl.glClientActiveTexture(GL10.GL_TEXTURE0);
                }
                
                mChipMesh.draw(gl, false);
            }                

            gl.glPopMatrix();
        }
    }
    
    void update(double deltaTime, NpCamera camera) {
        if (camera == null) {
            return;
        }

        // Light's position in object space
        float[] lightPosObj = new float[4];

        for (int i = 0; i < mN; i++) {
            mChipMatrices[i].computeMatrices(mField, i, camera);
         
            if (mContextSupportsCubemaps) {
                mChipMatrices[i].applyInverseMat(mLightPos, lightPosObj);
                
                mChipMesh.computeTangentSpaceLight(lightPosObj, 
                                                   mChipTangentSpaceLight[i]);
                
                mChipTangentSpaceLightBuf[i].put(mChipTangentSpaceLight[i]);
                mChipTangentSpaceLightBuf[i].rewind();
            }
        }
        
        float boxScale = (float) mField.Dim / BOX_DEFAULT_DIM;
        
        mBoxWorldMat.loadIdentity();
        mBoxWorldMat.translate(mDim * 0.5f, -mDim * 0.5f, 0);
        mBoxWorldMat.scale(boxScale, boxScale, 1);
        
        camera.getView(mBoxWorldViewMat.getArray());
        mBoxWorldViewMat.multiply(mBoxWorldMat);
        
        if (mContextSupportsCubemaps) {
            mBoxWorldMat.computeInverse(mBoxInverseWorldMat);

            mBoxInverseWorldMat.multiplyVec(mLightPos, lightPosObj);

            mBoxMesh.computeTangentSpaceLight(lightPosObj, mBoxTangentSpace);

            mBoxTangentSpaceBuf.put(mBoxTangentSpace);
            mBoxTangentSpaceBuf.rewind();
        }
        
        mBG.update(deltaTime);
    }
}

final class ChipMatrixStruct {
    final private NpMatrix4 mWorldView = new NpMatrix4();
    final private NpMatrix4 mWorldViewProj = new NpMatrix4();
    final private NpMatrix4 mWorld = new NpMatrix4();
    final private NpMatrix4 mWorldBase = new NpMatrix4();
    final private NpMatrix4 mInverse = new NpMatrix4();
    
    private final int mDim; 
    
    private boolean mValid = false;
    
    public ChipMatrixStruct(int dim) {
        mDim = dim;
    }
    
    public void applyInverseMat(final float[] src, float[] dst) {
        if (mValid) {
            mInverse.multiplyVec(src, dst);
        } else {
            dst[0] = 0;
            dst[1] = 0;
            dst[2] = 1;
            dst[3] = 1;
            
            Log.w(LogTag.TAG, "inverse mat is not valid!");
        }
    }

    public void computeMatrices(ChipField field, int chipIndex, 
            NpCamera camera) {
        
        if (camera == null) {
            mValid = false;
            return;
        }
        
        int column = chipIndex % mDim;
        int row = chipIndex / mDim;
        
        mWorld.loadIdentity();
        mWorld.translate(column * Chip.OFFSET, -row * Chip.OFFSET, 0);

        mWorld.toMatrix(mWorldBase);
        
        Chip c = field.getChip(chipIndex);
        
        if ((c != null) && field.isSliding(c.mNum)) {
            switch (field.getSlidingDir()) {
            case DOWN:
                mWorld.translate(0, field.getSlide() * Chip.OFFSET, 0);
                break;
                
            case UP:
                mWorld.translate(0, -field.getSlide() * Chip.OFFSET, 0);
                break;
                
            case LEFT:
                mWorld.translate(-field.getSlide() * Chip.OFFSET, 0, 0);
                break;
                
            case RIGHT:
                mWorld.translate(field.getSlide() * Chip.OFFSET, 0, 0);
                break;
            }
        }
        
        mValid = mWorld.computeInverse(mInverse);
        
        camera.getView(mWorldView.getArray());
        mWorldView.multiply(mWorld);
        
        mWorldView.toMatrix(mWorldViewProj);
        mWorldViewProj.multiply(camera.getProj());
    }
    
    public boolean glLoadWorldMat(GL10 gl) {
        if ((gl == null) || (!mValid)) {
            return false;
        }
        
        gl.glLoadMatrixf(mWorldView.getArray(), 0);
        return true;
    }
    
    public void mulVecWorld(float[] in, float[] out) {
        mWorld.multiplyVec(in, out);
    }
}

class PointerStruct {
    int mPointerId = 0;

    private float mInitialX = 0;
    private float mInitialY = 0;
    
    private float mX = 0;
    private float mY = 0;
    
    private float mDeltaX = 0;
    private float mDeltaY = 0;

    float getDeltaX() {
        return mDeltaX;
    }
    
    float getDeltaY() {
        return mDeltaY;
    }
    
    float getInitialDeltaX() {
        return mX - mInitialX;
    }
    
    float getInitialDeltaY() {
        return mY - mInitialY;
    }
    
    float getInitialX() {
        return mInitialX;
    }
    
    float getInitialY() {
        return mInitialY;
    }
    
    float getX() {
        return mX;
    }
    
    float getY() {
        return mY;
    }
    
    void goDown(float x, float y) {
        mInitialX = x;
        mInitialY = y;
        
        mX = x;
        mY = y;
        
        mDeltaX = 0;
        mDeltaY = 0;
    }
    
    void Move(float x, float y) {
        mDeltaX = x - mX;
        mDeltaY = y - mY;

        mX = x;
        mY = y;
    }
}

public class StateGame extends NpGameState implements FifteenConsts {

    private class CameraMotionEvent extends MotionEventPolicy {
        
        private double mZ = 0;
        private double mX = 0;
        private boolean mFailed = false;
        
        CameraMotionEvent() {
            preEvent();
        }
        
        @Override
        boolean postEvent() {
            
            if (!super.postEvent()) {
                return false;
            }
            
            if ((mPointerCnt != 2) || (mPointerCntPrev != 2)) {
                mFailed = true;
            }
            
            if (mFailed) {
                return true;
            }
            
            double t = Math.atan2(mPointers[1].getY() - mPointers[0].getY(),
                                  mPointers[1].getX() - mPointers[0].getX());

            double dz = t - mZ;

            if (dz < -Math.PI) {
                dz += 2 * Math.PI;
            } else if (dz > Math.PI) {
                dz -= 2 * Math.PI;
            }

            dz *= 180 / Math.PI;

            mCameraSpeedZ -= 3 * dz;

            mX -= (mPointers[1].getY() + mPointers[0].getY()) * 0.5;

            mCameraSpeedX -= mX;

            updateCamPos();
            
            return true;
        }
        
        @Override
        boolean preEvent() {
            if (!super.preEvent()) {
                return false;
            }
            
            if (mPointerCnt != 2) {
                return true;
            }
            
            mZ = Math.atan2(mPointers[1].getY() - mPointers[0].getY(),
                            mPointers[1].getX() - mPointers[0].getX());

            mX = (mPointers[1].getY() + mPointers[0].getY()) * 0.5;

            return true;
        }
    }        
    
    private class ChipMotionEvent extends MotionEventPolicy {
        
        private final float[] mOrigin = new float[3];
        private final boolean mValid;
        private final int mPointerId;
        
        ChipMotionEvent() {
            mValid = computeRaySceneIntersection(mOrigin, 
                                                 mPointers[0].getInitialX(), 
                                                 mPointers[0].getInitialY());
            
            mPointerId = mPointers[0].mPointerId;
        }
        
        private boolean computeRaySceneIntersection(float[] out, 
                float x, float y) {
                
            float[] rayA = new float[4];
            float[] rayB = new float[4];
            
            getPointerSceneRay(x, y, rayA, rayB);

            float[] p = {0, 0, Chip.CENTER[2]};

            NpMath.calcLinePlaneIntersection(p, NpVec3.kOrt3, 
                                             rayA, rayB, out);

            return true;
        }
        
        @Override
        boolean postEvent() {
            
            if (!super.postEvent()) {
                return false;
            }
            
            if (!mValid) {
                return true;
            }
            
            if (mIsSolved || mIsChipsLocked) {
                return true;
            }

            ChipSlidingDir dir = mChipField.getSlidingDir();
            
            if (dir == ChipSlidingDir.LOCKED) {
                return true;
            }
            
            float[] p = new float[3];

            PointerStruct ps = null;
            
            for (int i = 0; i < mPointerCnt; i++) {
                if (mPointers[i].mPointerId == mPointerId) {
                    ps = mPointers[i];
                    break;
                }
            }
            
            if (ps == null) {
                return true;
            }
            
            if (!computeRaySceneIntersection(p, ps.getX(), ps.getY())) {
                return true;
            }
            
            float dt = 0;
            
            switch (dir) {
            case LEFT:
                dt = (mOrigin[0] - p[0]);
                break;

            case RIGHT:
                dt = (p[0] - mOrigin[0]);
                break;
                
            case UP:
                dt = (mOrigin[1] - p[1]);
                break;

            case DOWN:
                dt = (p[1] - mOrigin[1]);
                break;
            }

            dt = (dt < 0) ? 0 : dt;
            dt = (dt > 1) ? 1 : dt;

            mChipField.setSlide(dt);
            
            return true;
        }
    }
    
    private class MotionEventPolicy {
        
        protected int mPointerCntPrev = 0;
        
        boolean postEvent() {
            return mPointerCnt > 0;
        }
        
        boolean preEvent() {
            mPointerCntPrev = mPointerCnt;
            
            return mPointerCntPrev > 0;
        }
    }

    private static final NpFontParams NEURO24 = 
        new NpFontParams("neuro24", 24, COLOR_WHITE);
    
    private static final float MOTION_SPEED_ATTENUATION = 4;

    private static final int MAX_POINTERS = 5;
    
    private final NpClickListener mZoomListener = new NpClickListener() {
        @Override
        void onClick() {
            mIsCameraRestoring = true;
        }
    };
    
    private final NpClickListener mMenuListener = new NpClickListener() {
        @Override
        void onClick() {
            mBackHit = true;
        }
    };
    
    private long mTimeStamp = 0;

    private NpCamera mCamera = new NpCamera(1.0f);
    
    private final ChipField mChipField;
    private final ChipFieldView mChipFieldView;
    private boolean mIsSolved = false;

    private float mCameraAngleX = -30;
    private float mCameraAngleZ = 0;
    
    private float mCameraSpeedX = 0;
    private float mCameraSpeedZ = 0;
    
    private final PointerStruct[] mPointers = new PointerStruct[MAX_POINTERS];
    private int mPointerCnt = 0;
    
    // mIsCameraRestoring - restoring camera angle for top-down view.
    private boolean mIsCameraRestoring = false;
    
    // mBackHit - someone pressed the back-button on the device, 
    //            should pop our state to go for the menu state.
    private boolean mBackHit = false;

    private boolean mIsMoving = false;
    
    private boolean mIsChipsLocked = false;
    
    private MotionEventPolicy mMotionEventHandler = null;
    
    private final HallOfFame mRecords;

    private boolean mShowEndDialog = false;

    private final NpSoundMan mSoundMan;
    
    private long timeElapsed = 0;

    public StateGame(NpGame g, GL10 gl, AssetManager assets, int puzzleSize) {
        super(g, gl, assets);
        
        for (int i = 0; i < mPointers.length; i++) {
            mPointers[i] = new PointerStruct();
        }
        
        mSoundMan = NpSoundMan.getInstance(g.getActivity());
        
        mChipField = new ChipField(puzzleSize, 
                                   NpSoundMan.getInstance(g.getActivity()));
        
        mChipFieldView = new ChipFieldView(gl, g.getActivity(), assets, 
                                           mChipField);
     
        mRecords = HallOfFame.getInstance(g.getActivity(), 
                                          HallOfFame.get15HallName(puzzleSize));
        
        loadAssets(gl, assets);
        
        mTimeStamp = System.nanoTime();
        
        Panels.disableTopBtns();
        Panels.enableBottomBtns();
    }
    
    public void getPointerSceneRay(float x, float y, 
            float[] rayA, float[] rayB) {
        
        NpMatrix4 m = new NpMatrix4();
        mCamera.getInvViewProj(m.getArray());

        float[] screenPos = 
                {2 * (x / getSurfaceWidth()) - 1, 
                1 - 2 * (y / getSurfaceHeight()), 
                0, 1};

        m.multiplyVec(screenPos, rayA);
        NpVec4.clip(rayA);

        screenPos[2] = 1;

        m.multiplyVec(screenPos, rayB);
        NpVec4.clip(rayB);
    }

    private int getOrCreatePointerStructId(int pointerId) {
        int found = getPointerStructId(pointerId);
        
        if ((found < 0) && (mPointerCnt < mPointers.length)) {
            found = mPointerCnt;
            mPointers[found].mPointerId = pointerId;
            mPointerCnt++;
        }
        
        return found;
    }
    
    private int getPointerStructId(int pointerId) {
        
        for (int j = 0; j < mPointerCnt; j++) {
            if (mPointers[j].mPointerId == pointerId) {
                return j;
            }
        }
        return -1;
    }
    
    @Override
    public void handleMotionEvent(MotionEvent event) {
        
        if ((mMotionEventHandler != null) && !mMotionEventHandler.preEvent()) {
            mMotionEventHandler = null;
        } 
        
        int preCnt = mPointerCnt;
        
        int action = event.getAction();
        int actionCode = action & MotionEvent.ACTION_MASK;

        int rawId = action & MotionEvent.ACTION_POINTER_ID_MASK;
        rawId >>= MotionEvent.ACTION_POINTER_ID_SHIFT;
    
        int pointerId = event.getPointerId(rawId);
        int id = getOrCreatePointerStructId(pointerId);
        
        if (id >= 0) {
            switch (actionCode) {
            case MotionEvent.ACTION_DOWN:
            case MotionEvent.ACTION_POINTER_DOWN:
                mPointers[id].goDown(event.getX(), event.getY());
                break;

            case MotionEvent.ACTION_MOVE:
                mPointers[id].Move(event.getX(), event.getY());
                break;

            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_POINTER_UP:
                if (mPointerCnt > 0) {
                    mPointerCnt--;

                    if (mPointerCnt > 0) {
                        PointerStruct p = mPointers[id];
                        mPointers[id] = mPointers[mPointerCnt];
                        mPointers[mPointerCnt] = p;
                    }
                }
                break;
            
            case MotionEvent.ACTION_CANCEL:
                mPointerCnt = 0;
                break;
            }
        }
        
        for (int i = 0; i < event.getPointerCount(); i++) {
            
            if (i == rawId) {
                continue;
            }
            
            pointerId = event.getPointerId(i);
            
            id = getPointerStructId(pointerId);
            
            if (id < 0) {
                continue;
            }
            
            mPointers[id].Move(event.getX(i), event.getY(i));
        }
        
        if (!mIsMoving) {
            if (mMotionEventHandler == null) {
                
                if (preCnt == 0) {
                    updateSlidingChips();
                }
                
                boolean isGesture = false;
                
                for (int i = 0; i < mPointerCnt; i++) {
                    PointerStruct p = mPointers[i];
                    
                    if ((Math.abs(p.getInitialDeltaX()) > 3) 
                            || (Math.abs(p.getInitialDeltaX()) > 3)) {
                        isGesture = true;
                        break;
                    }
                }
                
                if (isGesture) {
                    if (mPointerCnt == 2) {
                        mMotionEventHandler = new CameraMotionEvent();
                    } else if (mPointerCnt == 1) {
                        mMotionEventHandler = new ChipMotionEvent();
                    }
                }
            }
            
            if (mMotionEventHandler != null) { 
                if (!mMotionEventHandler.postEvent()) {
                    mMotionEventHandler = null;
                }
            }
        }
        if (Log.isLoggable(LogTag.TAG, Log.INFO)) {
            Log.i(LogTag.TAG, "pointersCnt == " + mPointerCnt);
        }
    }
    
    private void loadAssets(GL10 gl, AssetManager assets) {
        try {
            mChipFieldView.loadAssets(gl);
        } catch (IOException e) {
            LogHelper.e("mChipFieldView.loadAssets failed: " + e.getMessage());
        }
    }
    
    @Override
    protected void onRelease(GL10 gl) {
        mChipFieldView.releaseAssets(gl);
    }
    
    @Override
    protected boolean onBackPressed() {
        mBackHit = true;
        return true;
    }
    
    @Override
    protected void onForeground() {
        mTimeStamp = System.nanoTime();
        Panels.disableTopBtns();
        Panels.enableBottomBtns();
    }
    
    @Override
    public void refreshContextAssets(GL10 gl, EGLConfig config, 
            AssetManager assets) {
        super.refreshContextAssets(gl, config, assets);
        loadAssets(gl, assets);
    }
    
    @Override
    public void render(GL10 gl) {
        if (mBackHit) {
            mBackHit = false;
            // pushing Menu state is delayed for valid gl entity
            Panels.setActionOnClosed(new Panels.NpActionWithGL() {
                @Override
                void action(GL10 gl) {
                    mGame.pushState(new StateMenu(mGame, gl, mAssets));  
                }
            });
        }
        
        gl.glColor4f(1, 1, 1, 1);
        gl.glClearColor(0, 0, 0.25f, 0);
        gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
        
        gl.glMatrixMode(GL10.GL_PROJECTION);
        gl.glLoadMatrixf(mCamera.getProj(), 0);
        
        gl.glMatrixMode(GL10.GL_MODELVIEW);
        gl.glLoadMatrixf(mCamera.getView(), 0);

        gl.glClientActiveTexture(GL10.GL_TEXTURE0);
        gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
        gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
        
        mChipFieldView.render(gl);
        
        // gui goes here
        gl.glColor4f(1, 1, 1, 1);

        NpGuiState.prepare(gl, getSurfaceWidth(), getSurfaceHeight());
        
        Panels.preRender(mSoundMan, gl);
        
        Panels.movesCount = mChipField.getMovesCnt();
        
        Panels.drawBottomPane(DEF_WIDTH, DEF_HEIGHT, 
                              mMenuListener, mZoomListener);
        
        long curTime = System.currentTimeMillis();
        
        if (mShowEndDialog) {
            renderEndDialog(gl, curTime);
        }
        
        Panels.time = timeElapsed;
        
        Panels.drawTopPanel(DEF_WIDTH, DEF_HEIGHT);
        
        NpGuiState.finish();
        
        Panels.postRender();
        
        FifteenButton.postRender(curTime);
    }
    
    public void renderEndDialog(GL10 gl, long curTime) {
        FifteenButton fb = FifteenButton.getInstance(mSoundMan);
        
        final int TITLE_PADDING = 20;
        
        final String exitCap = "You win!!";

        final NpRect exitRect = NpSkin.computeTextRect(NEURO24.name, 
                                                       NEURO24.height, 
                                                       exitCap);
        
        // window height
        int wh = TITLE_PADDING + exitRect.h + FifteenButton.BUTTON_PADDING 
                   + (FifteenButton.BUTTON_H_NORMAL + FifteenButton.BUTTON_PADDING) * 2;

        // window width
        int ww = (int) Math.max(DEF_WIDTH * 0.75, 
                                exitRect.w + exitRect.x + 20);
        
        NpRect wRect = new NpRect((DEF_WIDTH - ww) / 2, (DEF_HEIGHT - wh) / 2, 
                                  ww, wh);

        NpSkin.doRectWidget(NpWidgetState.WS_NORMAL, "Window", wRect);
        
        NpSkin.doLabel(wRect.x + wRect.w / 2, 
                       wRect.y + FifteenButton.BUTTON_PADDING, 
                       exitCap, NEURO24, 
                       NpAlignConsts.ALIGN_CENTER);

        int by = wRect.y + wRect.h - FifteenButton.BUTTON_H_NORMAL 
                 - FifteenButton.BUTTON_PADDING;

        if (clicked(fb.doButton("New Game", by, curTime))) {
            final int puzzleSize = mChipField.Dim;
            Panels.setActionOnClosed(new Panels.NpActionWithGL() {
                @Override
                void action(GL10 gl) {
                    mGame.popState();
                    mGame.pushState(new StateGame(mGame, gl, mAssets, 
                                                  puzzleSize));
                }
            });
        }

        by -= FifteenButton.BUTTON_H_NORMAL + FifteenButton.BUTTON_PADDING; 

        if (clicked(fb.doButton("Go to menu", by, curTime))) {
            Panels.setActionOnClosed(new Panels.NpActionWithGL() {
                @Override
                void action(GL10 gl) {
                    mGame.popState();
                }
            });
        }
    }
    
    @Override
    public void setupOnSurfaceChanged(GL10 gl, int width, int height) {
        super.setupOnSurfaceChanged(gl, width, height);
        
        //gl.glEnable(GL10.GL_CULL_FACE);
        gl.glDisable(GL10.GL_CULL_FACE);
        gl.glCullFace(GL10.GL_BACK);
        gl.glFrontFace(GL10.GL_CCW);
        
        gl.glDisable(GL10.GL_DEPTH_TEST);
        
        gl.glActiveTexture(GL10.GL_TEXTURE0);
        gl.glEnable(GL10.GL_TEXTURE_2D);

        gl.glDisable(GL10.GL_BLEND);

        if ((DEF_HEIGHT == height) && (DEF_WIDTH == width)) {
            NpSkin.disableScaling();
        } else {
            NpSkin.enableScaling((float) width / DEF_WIDTH, 
                                 (float) height / DEF_HEIGHT);
        }
        
        mCamera.onScreenAspectChanged((float) width / height);
        mCamera.setProjParams(45.0f, 0.125f, 20.0f);
        
        updateCamPos();
    }
    
    private void showGameEndDialog() {
        mShowEndDialog = true;
    }

    private void showLoginInputDialog(final int recordPos) {
        final Activity activity = mGame.getActivity(); 
        
        activity.runOnUiThread(new Runnable() {
            
            @Override
            public void run() {
                AlertDialog.Builder alert = new AlertDialog.Builder(activity);

                alert.setTitle("New record: " + (recordPos + 1) + " place!");
                alert.setMessage("Enter your name (" 
                                 + HallOfFame.MAX_NAME_LENGTH + " chars max, " 
                                 + "latin symbols only");

                // Set an EditText view to get user input 
                final EditText input = new EditText(activity);
                
                InputFilter[] filters = new InputFilter[2];
                filters[0] = 
                    new InputFilter.LengthFilter(HallOfFame.MAX_NAME_LENGTH);
                
                filters[1] = new InputFilter() {
                    public CharSequence filter(CharSequence source, int start, 
                            int end, Spanned dest, int dstart, int dend) {
                        
                        StringBuilder s = new StringBuilder();
                       
                        for (int i = start; i < end; i++) {
                            char c = source.charAt(i);
                            
                            if ((c >= 'A') && (c <= 'Z')) {
                                s.append(c);
                                continue;
                            }
                            
                            if ((c >= 'a') && (c <= 'z')) {
                                s.append(c);
                                continue;
                            }
                            
                            if ((c >= '0') && (c <= '9')) {
                                s.append(c);
                                continue;
                            }
                        }
                        
                        return s;
                    };
                };
                
                input.setFilters(filters);
                
                input.setMaxLines(1);
                alert.setView(input);

                OnClickListener listener = new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, 
                            int whichButton) {
                        String value = input.getText().toString();
                        
                        if ((value == null) || (value.length() == 0)) {
                            value = "unknown";
                        }
                        
                        mRecords.putValue(value, mChipField.getMovesCnt());
                        
                        Panels.setActionOnClosed(new Panels.NpActionWithGL() {
                            @Override
                            void action(GL10 gl) {
                                mGame.popState();
                            }
                        });
                    }
                };
        
                alert.setPositiveButton("Ok", listener);
                alert.setCancelable(false);
                alert.show();
            }
        });
    }

    private void showWinDialog() {
        
        int recordPos = mRecords.getResultPos(mChipField.getMovesCnt());
        
        if (recordPos >= 0) {
            showLoginInputDialog(recordPos);
        } else {
            showGameEndDialog();
        }
    }

    @Override
    public void update() {
        super.update();
        
        long nanoTime = System.nanoTime();
        long deltaTimeInNano = nanoTime - mTimeStamp;
        
        if (deltaTimeInNano > 0) {
            if (mChipField.isSolved() && !mIsSolved) {
                mIsSolved = true;
                
                mIsChipsLocked = true;
                
                showWinDialog();
            }
            if (!mIsSolved){
            	timeElapsed += deltaTimeInNano;
            }

            double deltaTimeInSecs = deltaTimeInNano / 1.0E+09d; 
            
            updateCamera(deltaTimeInSecs);

            Panels.update(mSoundMan, deltaTimeInSecs);
            
            if ((mMotionEventHandler == null) && (mPointerCnt == 0)) {
                mIsMoving = mChipField.updateChipMoves(deltaTimeInSecs);
            }
            
            mChipFieldView.update(deltaTimeInSecs, mCamera);
            
            mTimeStamp = nanoTime;
        }
    }

    private void updateSlidingChips() {
        if (mPointerCnt != 1) {
            return;
        }
        
        Log.i(LogTag.TAG, "updating sliding chips");
        
        float[] a = new float[4];
        float[] b = new float[4];
        
        getPointerSceneRay(mPointers[0].getX(), mPointers[0].getY(), a, b);
        int activeChip = mChipFieldView.rayToChip(a, b);

        mChipField.setSlidingChips(activeChip);
    }
    
    private void updateCamera(double deltaTime) {

        double speedAtten = 1 - MOTION_SPEED_ATTENUATION * deltaTime;
        
        speedAtten = NpMath.clampd(speedAtten, 0, 1);
        
        if (mIsCameraRestoring) {
            mCameraAngleX *= speedAtten;
            mCameraAngleZ *= speedAtten;
            mCameraSpeedX = 0;
            mCameraSpeedZ = 0;
            
            // if angle is lesser than 1 degree - stop it 
            if ((Math.abs(mCameraAngleX) < 1) 
                    && (Math.abs(mCameraAngleZ) < 1)) {
                
                mCameraAngleX = 0;
                mCameraAngleZ = 0;
                
                mIsCameraRestoring = false;
            }
            updateCamPos();
            return;
        }
        
        boolean needUpdateCamPos = false;
        
        if (Math.abs(mCameraSpeedX) > NpMath.ZERO) {
            mCameraAngleX += mCameraSpeedX * deltaTime;
            mCameraSpeedX *= speedAtten;
            
            needUpdateCamPos = true;
        }
        
        if (Math.abs(mCameraSpeedZ) > NpMath.ZERO) {
            mCameraAngleZ += mCameraSpeedZ * deltaTime;
            mCameraSpeedZ *= speedAtten;
            
            needUpdateCamPos = true;
        }
        
        if (needUpdateCamPos) {
            mCameraAngleX = NpMath.clampf(mCameraAngleX, -80, 0);

            if (mCameraAngleZ < -360) {
                mCameraAngleZ += 360;
            } else if (mCameraAngleZ > 360) {
                mCameraAngleZ -= 360;
            }

            updateCamPos();
        }
    }
    
    void updateCamPos() {
        NpMatrix4 m = new NpMatrix4();
        
        int puzzleSize = mChipField.Dim;
        
        m.translate(0, 0, -5);
        m.rotate(mCameraAngleX, 1, 0, 0);
        m.rotate(mCameraAngleZ, 0, 0, 1);
        m.translate(-puzzleSize * 0.5f, puzzleSize * 0.5f, 0);
        
        mCamera.setView(m.getArray());
    }

}
